

--[[
    服务端启动流程说明：
    1.查询mysql,查到所有相机id,ip信息，以及所有服务器节点，放入固定全局table中
    2.向所有节点发送monitor_all命令，获取每台服务器所布防的相机以及可以判断出此时节点的状态(初始化状态)
    3.新增节点，新增的节点状态必须在线
    4.轮询相机id，查看其实际接管的服务器并与当前接管服务器进行比较，已布防的过滤掉，对应的相机未布防的在其它节点上先撤防再布防。
    5.更新ssdb
    多个监控程序，可能会造成混乱，比如监控程序1检测到所有节点服务器状态都在线，与此同时监控程序2网络不通检测到节点1不在线，这时相机布防会发生混乱。
    定时任务，每10min发送一次check命令检查一次服务器节点状态，更新ssdb。(libev定时任务)
    发现服务器节点状态不在线，重新布防相机到其它节点。
    以ip:端口作为服务器节点，可以实现同一台服务器开启多个节点。
    mysql 字段(待定)
    相机配置表:
    (主键)相机id 相机ip 相机端口 相机用户名 相机密码 布防模式 布防等级 发送的url 重连次数 重连时间
    服务器节点表
    (主键)id  服务器ip 服务器端口(zmq端口)
    相机信息表
    相机类型  重连时间 超时时间 重连次数
    ssdb数据结构
    key                       value
    相机id                    相机id,接管服务器ip(含端口),相机状态，服务器节点状态(含端口)
    
--]] 


package.path = '/usr/local/KKCJ_LUA_NEW/gcsfun/?.lua;'.. package.path
--package.cpath = '/usr/lib/?.so;'.. package.cpath
--package.path = '/Users/hongliang/Documents/LuaCode/KKCJ_LUA_NEW/gcsfun/?.lua;'.. package.path
package.cpath = '/usr/lib/?.so;'.. package.cpath

local consistent = require "client.consistent"         ----hash
local utils = require "utils"
local cjson = require "cjson"
cjson.encode_sparse_array(true)               ----有的table内容过于稀疏的队列，将encode_sparse_array设为true,使其能打印过于稀疏的table
local zmq = require "lzmq"
local config_utils = utils.config
local log_utils = utils.log
local db_utils = utils.db
local libev_utils = utils.libev

local log = log_utils:new("debug","./gcs.log")
local tb_config = config_utils.read('./gcs_config.conf')

local tb_all_cms = {}           ----所有相机的信息     {[id]={id=,user=,pass=},[id]={id=,user=,pass=}}
local tb_new_nodes = {}        ----所有服务器节点信息    {[node]={ip=192.168.1.1,node=192.168.1.1:5555,status,skt},[node]={ip=192.168.1.1,node=192.168.1.1:5556,status=},n=2}
                                  ----status 0表示节点状态正常,1表示异常
--local tb_old_servers = {}        ----ssdb中记录的服务器节点信 {{}}
local tb_new_cms = {}            ----新的相机应布防tb,10min之后更新的记录     {[id]={id=,ip=,node=},[id]={id=,ip=,node=}}
local tb_old_cms = {}            ----旧的相机布防tb,10min之前ssdb中的记录     {}
local tb_zmq = {}                ----连接zmq服务器节点的对象 {[node]={ip=,skt=,node=}},[node]={skt=}}

----连接所有服务器节点,获得zmq对象,放入table中

----函数:校验2个table,布防    传参数tb1,tb2    返参数tb{add:{{}},del:{{}}}

----解析获取的tb，先撤防，再布防


---定义全局事件驱动器
local loop = libev_utils.Loop()

    local mysql_db,err = db_utils:_mysql_new(tb_config)     ----初始化mysql对象
    if not mysql_db then
        print("mysql 对象初始化失败,err:"..err)
        os.exit()
    end
    print("mysql 对象初始化成功")
    
print("tb_config",cjson.encode(tb_config))

local ctx = zmq.context()

local function _new_zmq()
    local tb_return = {}
    for node,v in pairs(tb_new_nodes) do
        local skt,err = ctx:socket{zmq.REQ,
        linger = 0, rcvtimeo = tb_config.zmq_timeout;
        connect = "tcp://"..node
        }
        if not skt then
            return nil,"socket connect err:"..err
        end
        
        local tb_tmp = {}
        tb_tmp.skt = skt
        tb_tmp.ip = v.ip
        tb_tmp.node = node
        tb_return[node] = tb_tmp
    end

    return tb_return
end

-----1.增加节点
---2.获取相机id布防的节点
--3.返回这些id布防的节点{}
local function _do_hash(tb_nodes)          -----分配相机到服务器节点
    local tb_return = {}                      ------{[id]={id=,ip=,node=},[id]={id=,ip=,node=}}
    consistent.init()                             ---增加节点前先初始化一次
    local nodes = {}
    for node,v in pairs(tb_nodes) do
        table.insert(nodes,node)
    end
    consistent.add_server(nodes)           ----增加节点
    for id,v in pairs(tb_all_cms) do
         local node = consistent.get_upstream(id)
         tb_return[id] = {}
         tb_return[id].id = id
         tb_return[id].ip = tb_nodes[node].ip
         tb_return[id].node = node
    end
    return tb_return
end 

--------1.将tb_new节点状态为1的剔除生成新的
--2.最新相机分配,更新tb_new_cms
--3.对比tb_new_cms与tb_old_cms
--3.命令布防、撤防调度相机
function check_camera()
    local _new_nodes = {}                   
    for _i,_v in pairs(tb_new_nodes) do
        local status = _v.status
        if tonumber(status) == 0 then
            _new_nodes[_i] = _v                 ---_new_nodes为{},表示所有节点都异常,可能是自己与节点之间断网,此时不要做任何操作
        end
    end
    if not next(_new_nodes) then
        print("所有节点异常")
        return " "
    end
    print("_new_nodes",cjson.encode(_new_nodes))
    ----按照最新的节点分配相机放入tb_new_cms中,根据tb_all_cameras与_new_nodes两张表
    tb_new_cms = _do_hash(_new_nodes)       ----新布防相机的table中没有相机状态
     
     print("tb_new_cms",cjson.encode(tb_new_cms))
    ----比较tb_new_cms与tb_old_cms     {id={}}
    ---比较方法:
    --先判断tb_old_cms是否为空table,如果为空,全部布防,
    --不为空,比较tb_old_cms与tb_new_cms,找出哪些相机需要布防,哪些需要撤防
    --1.第一轮 轮询2个table,筛选出哪些相机需要撤防(要撤防的相机状态必须为0)、哪些相机需要布防
    --2.第二轮 先撤防这些相机,再布防
    print("开始比较tb_new_cms与tb_old_cms)")
    print("tb_old_cms:"..cjson.encode(tb_old_cms))
    print("tb_new_cms:"..cjson.encode(tb_new_cms))
    if not next(tb_old_cms) then        ---所有相机都未布防在节点中,布防所有相机
        for node,v in pairs(_new_nodes) do
            local tb_add_data = {}   ---布防table
            local tb_content = {}
            local skt = tb_zmq[node].skt
            for id,v1 in pairs(tb_new_cms) do
                -----{"host":"192.168.1.64","ptc_level":0,"id":1,"user":"admin","ptc_mod":0,"pass":"12345","port":"8000"}
                if v1.node == node then
                    local tb_id_v = {}
                    tb_id_v.id = id
                    tb_id_v.user = tb_all_cms[id].user              ------tb_all_cms中记录的相机用户名、密码
                    tb_id_v.pass = tb_all_cms[id].pass
                    tb_id_v.host = tb_all_cms[id].host
                    tb_id_v.ptc_level = tb_all_cms[id].ptc_level
                    tb_id_v.ptc_mod = tb_all_cms[id].ptc_mod
                    tb_id_v.port =  tb_all_cms[id].port
                    tb_id_v.camera_type = tb_all_cms[id].camera_type
                    tb_id_v.time_out = tb_all_cms[id].time_out
                    tb_id_v.recon_time = tb_all_cms[id].recon_time
                    tb_id_v.con_num = tb_all_cms[id].con_num
                    table.insert(tb_content,tb_id_v)
                end
            end
            tb_add_data.cmd = "add"
            tb_add_data.url = v.url
            tb_add_data.curl_timeout = v.curl_timeout
            tb_add_data.content = tb_content                -----有的节点可能没有分配相机,过滤掉这些节点
            if next(tb_content) then
                local send_data = cjson.encode(tb_add_data)
                skt:send(send_data)
                local res_data = skt:recv()
                print("所有节点都未布防相机,发送布防命令,发送节点:"..node)
                print("发送内容:"..send_data)
                print("收到内容:"..res_data)
                if not res_data then
                    print("发送布防命令失败,节点可能异常,异常节点:"..node)
                else
                    local tb_data = cjson.decode(res_data)
                                                                ----更新tb_new_cms中相机的状态
                    for _,v in pairs(tb_data) do
                        tb_new_cms[v.id].status = v.status
                    end
                    
                end
            end
            
        end 
        
    else
        local tb_add_cms = {}       --{[node]={{id=,},{id=,}}}
        local tb_del_cms = {}
        for id,v in pairs(tb_new_cms) do
            tb_add_cms[v.node] = tb_add_cms[v.node] or {}                 --初始化tb_add_cms[v.node]的值为{}
            
            if not tb_old_cms[id] then          ----相机目前未布防在任何服务器上,布防相机
                local tb_id_v = {}
                tb_id_v.id = id
                tb_id_v.user = tb_all_cms[id].user              ------tb_all_cms中记录的相机用户名、密码
                tb_id_v.pass = tb_all_cms[id].pass
                tb_id_v.host = tb_all_cms[id].host
                tb_id_v.ptc_level = tb_all_cms[id].ptc_level
                tb_id_v.ptc_mod = tb_all_cms[id].ptc_mod
                tb_id_v.port =  tb_all_cms[id].port
                tb_id_v.camera_type = tb_all_cms[id].camera_type
                tb_id_v.time_out = tb_all_cms[id].time_out
                tb_id_v.recon_time = tb_all_cms[id].recon_time
                tb_id_v.con_num = tb_all_cms[id].con_num
                table.insert(tb_add_cms[v.node],tb_id_v)
            else
                tb_del_cms[tb_old_cms[id].node] = tb_del_cms[tb_old_cms[id].node] or {}   --初始化tb_del_cms[v.node]的值为{}
                if tb_old_cms[id].node ~= v.node then   ---节点目前没有布防相机,撤防、布防相机,状态等于0的撤防,不为0的直接布防到新的节点
                    if tonumber(tb_old_cms[id].status) == 0 then    ---此id被其它节点布防,需要从其它节点撤防,撤防之后再将id布防到现在的节点
                        local tb_id_v = {}
                        tb_id_v.id = id
                        table.insert(tb_del_cms[tb_old_cms[id].node],tb_id_v)
                    end
                    
                    local tb_id_v1 = {}
                    tb_id_v1.id = id
                    tb_id_v1.user = tb_all_cms[id].user              ------tb_all_cms中记录的相机用户名、密码
                    tb_id_v1.pass = tb_all_cms[id].pass
                    tb_id_v1.host = tb_all_cms[id].host
                    tb_id_v1.ptc_level = tb_all_cms[id].ptc_level
                    tb_id_v1.ptc_mod = tb_all_cms[id].ptc_mod
                    tb_id_v1.port =  tb_all_cms[id].port
                    tb_id_v1.camera_type = tb_all_cms[id].camera_type
                    tb_id_v1.time_out = tb_all_cms[id].time_out
                    tb_id_v1.recon_time = tb_all_cms[id].recon_time
                    tb_id_v1.con_num = tb_all_cms[id].con_num
                    table.insert(tb_add_cms[v.node],tb_id_v1)
                    
                else                                                -----此id被目前的节点布防,将id状态不为0的重新布防
                    if tonumber(tb_old_cms[id].status) == 0 then              ----已被布防,
                        v.status = tb_old_cms[id].status    -----更新tb_new_cms中id的状态
                    else    ----未被布防,重新布防
                        local tb_id_v = {}
                        tb_id_v.id = id
                        tb_id_v.user = tb_all_cms[id].user              ------tb_all_cms中记录的相机用户名、密码
                        tb_id_v.pass = tb_all_cms[id].pass
                        tb_id_v.host = tb_all_cms[id].host
                        tb_id_v.ptc_level = tb_all_cms[id].ptc_level
                        tb_id_v.ptc_mod = tb_all_cms[id].ptc_mod
                        tb_id_v.port =  tb_all_cms[id].port
                        tb_id_v.camera_type = tb_all_cms[id].camera_type
                        tb_id_v.time_out = tb_all_cms[id].time_out
                        tb_id_v.recon_time = tb_all_cms[id].recon_time
                        tb_id_v.con_num = tb_all_cms[id].con_num
                        table.insert(tb_add_cms[v.node],tb_id_v)
                        
                    end
                    
                end
            end
        
        end
        ------先发送撤防命令
        for node,v in pairs(tb_del_cms) do
            if next(v) then
                local tb_del_data = {}
                local skt = tb_zmq[node].skt
                tb_del_data.cmd = "del"
                tb_del_data.content = v
                local send_data = cjson.encode(tb_del_data)
                skt:send(send_data)
                local res_data,err = skt:recv()
                if not res_data then
                    print("发送撤防命令失败,节点可能异常,异常节点:"..node)
                else
                    print("发送撤防,发送内容:"..send_data)
                    print("发送撤防,收到内容:"..res_data)
                    local tb_data = cjson.decode(res_data)
                    for _,v1 in pairs(tb_data) do
                        if tonumber(v1.status) == 0 then   --撤防失败,修改tb_new_cms中将id布防的节点改成现在的,再将节点node中的id在tb_add_cms中将此移除
                            tb_new_cms[v1.id].node = node
                            tb_new_cms[v1.id].status = 0     ---修改状态
                            local tb_id_v = tb_add_cms[node]      ---移除add_cms中此id
                            for i,v2 in pairs(tb_id_v) do
                                if tonumber(v2.id) == tonumber(v1.id) then
                                    table.remove(tb_id_v,i)
                                end
                            end
                            
                        end
                        
                    end
                    
                end
                
            end
        end
        
        libev_utils.sleep(2)     ----sleep2秒,相机撤防后,再立即布防会失败
        ---------再发送布防命令
        for node,v in pairs(tb_add_cms) do
            if next(v) then
                local tb_add_data = {}
                local skt = tb_zmq[node].skt
                tb_add_data.cmd = "add"
                tb_add_data.url = tb_new_nodes[node].url
                tb_add_data.curl_timeout = tb_new_nodes[node].curl_timeout
                tb_add_data.content = v
                local send_data = cjson.encode(tb_add_data)
                skt:send(send_data)
                print("先撤放再布防后发送的布防的内容:"..send_data..",发送的布防节点:"..node)
                local res_data,err = skt:recv()
                print("收到的内容:"..res_data)
                if not res_data then
                    print("发送布防命令失败,节点可能异常,异常节点:"..node)
                else          ---布防所有相机,获取返回值更新tb_new_cms中相机状态
                    local tb_data = cjson.decode(res_data)
                    for _,v in pairs(tb_data) do
                        tb_new_cms[v.id].status = v.status
                    end
                    
                end
            end
            
        end
        
    end
    ----更新mysql,tb_new_nodes表与tb_new_cms表
    print("开始更新mysql")
    print("tb_new_nodes",cjson.encode(tb_new_nodes))
    print("tb_new_cms",cjson.encode(tb_new_cms))
    for node,v in pairs(tb_new_nodes) do
        local query = "update nodes set status = "..v.status.." where "
              query = query .. "node = '"..v.node.."'"
              print(query)
        mysql_db:query(query)
    end
    
    for id,v in pairs(tb_new_cms) do
        local query = "update cameras set status = "..v.status.." , "
              query = query .."node = '"..v.node.."' where "
              query = query .. "id = "..v.id
              print(query)
         mysql_db:query(query)
    end
    
    
end


------每秒钟检测一次节点状态,与上一秒对比,发现状态异常调用check_camera方法
function check_node()
    local tb_old_nodes = {}                   
    for node,v in pairs(tb_new_nodes) do                -----将tb_new_nodes赋值给tb_old_nodes
        tb_old_nodes[node] = {}
        for i,vv in pairs(v) do
            tb_old_nodes[node][i] = vv
        end
    end
    
    for node,v in pairs(tb_new_nodes) do        ---包含所有的节点,发送check命令
        local skt = tb_zmq[node].skt
        local tb_send_data = {}
        tb_send_data.cmd = "check"
        local send_data = cjson.encode(tb_send_data)
        skt:send(send_data)
        local res_data,err = skt:recv()
        if not res_data then             
            skt:close()
            skt,err = ctx:socket{zmq.REQ,
                                 linger = 0, rcvtimeo = tb_config.zmq_timeout;
                                 connect = "tcp://"..node              ----bind 
                                }
            tb_zmq[node].skt = skt
            skt:send(send_data)
            res_data,err = skt:recv()
            if not res_data then
                print("节点异常,节点:"..node..",异常信息:"..tostring(err))
                tb_new_nodes[node].status = -1
            else
                tb_new_nodes[node].status = 0
            end
        else
            tb_new_nodes[node].status = 0
        end
        
    end
    
    ----比较tb_new_nodes与tb_old_nodes
    local n = 0
    for node,v in pairs(tb_old_nodes) do
        if tonumber(v.status) ~= tonumber(tb_new_nodes[node].status) then
            n = n+1
            if tonumber(v.status) == 0 then
                print("check命令检测到节点状态与之前不一样,节点由正常变为异常,节点:"..node)
            else
                print("check命令检测到节点由异常恢复,节点:"..node)
            end
            
        end
    end
    if n > 0 then     ----n>0说明节点有异常
        init()
        check_camera()
    end
    
end


function init()                  ----初始化所有的配置信息,全局tb信息
    local tb_cms,err = mysql_db:query("select * from cameras")       ------tb_all_cms
    if not tb_cms then
      print("mysql 查询失败,err:"..err)
      os.exit()
    end
    
    for _,v in pairs(tb_cms) do
        tb_all_cms[v.id] = v              -----其中的ip为相机的ip,非相机布防的服务器ip
    end
    print("初始化 tb_all_cms成功")
    
    local tb_nodes,err = mysql_db:query("select * from nodes")
    if not tb_nodes then
        print("mysql 查询失败,err:"..err)
        os.exit()
    end
    
    for _,v in pairs(tb_nodes) do
        tb_new_nodes[v.node] = v              --所有的节点信息
    end
    print("初始化 tb_new_nodes成功")
    
    tb_zmq,err = _new_zmq()     ---根据节点实例化zmq对象
    if not tb_zmq then
        print("初始化tb_zmq失败,err:"..err)
        os.exit()
    end
    print("初始化 tb_zmq成功")
    tb_old_cms = {}                     ----先初始化一次
    for node,v in pairs(tb_zmq) do    
        local skt = v.skt
        local ip = v.ip
        local cmd = {}
        cmd.cmd = "monitor_all"
        local send_data = cjson.encode(cmd)
        local res,err = skt:send(send_data)        
        local res_data,err = skt:recv()                       
        if not res_data then             -----res_data:{{id=,status=},{id=,status=}}或者为"{}",转了2次json
         ------res为nil,为服务端挂掉,socket断了,此时服务端可能重启了,不能确定服务端目前状态
         --需要重连n次,结合res_data确定服务器状态,如果还是重连失败,说明服务端连接异常掉线
            skt:close()
            skt,err = ctx:socket{zmq.REQ,
                                 linger = 0, rcvtimeo = tb_config.zmq_timeout;
                                 connect = "tcp://"..node              ----bind 
                                }
            v.skt = skt
            skt:send(send_data)
            res_data,err = skt:recv()
            if not res_data then
                print("发送monitor_all命令发现节点异常,节点:"..node..",异常信息:"..tostring(err))
                tb_new_nodes[node].status = -1
            else
                tb_new_nodes[node].status = 0
                local tb_recv = cjson.decode(res_data)
                if type(tb_recv) ~= "table" then
                    tb_recv = cjson.decode(tb_recv)
                    for _,_v in pairs(tb_recv) do
                        _v.node = node
                        _v.ip = ip
                        tb_old_cms[tonumber(_v.id)] = _v        ----更新先前相机布防table,ip为布防的服务器ip,非相机ip
                    end
                else
                    for _,_v in pairs(tb_recv) do
                        _v.node = node
                        _v.ip = ip
                        tb_old_cms[tonumber(_v.id)] = _v        ----更新先前相机布防table,ip为布防的服务器ip,非相机ip
                    end
                end
                
            end
            
            
        else
            print("发送mon_all命令,收到res_data:"..res_data)
            tb_new_nodes[node].status = 0                                             ----节点状态已更新
            local tb_recv = cjson.decode(res_data)
            if type(tb_recv) ~= "table" then
                    tb_recv = cjson.decode(tb_recv)
                    for _,_v in pairs(tb_recv) do
                        _v.node = node
                        _v.ip = ip
                        tb_old_cms[tonumber(_v.id)] = _v        ----更新先前相机布防table,ip为布防的服务器ip,非相机ip
                    end
                else
                    for _,_v in pairs(tb_recv) do
                        _v.node = node
                        _v.ip = ip
                        tb_old_cms[tonumber(_v.id)] = _v        ----更新先前相机布防table,ip为布防的服务器ip,非相机ip
                    end
                end
            
        end
         
    end
    
    print("正在布防的信息,tb_old_cms",cjson.encode(tb_old_cms))
end

init()
check_camera()


local cb_monitor_all = function(loop, timer, revents)   ---2min执行一次monitor_all命令,查看相机状态
    print("this is monitor_all")
    init()
    check_camera()
end


local cb_check = function(loop, timer, revents)
    print("this is check")
    check_node()
end

local timer = libev_utils.Timer(cb_monitor_all,tb_config.monitor_all_time,tb_config.monitor_all_time)
local timer2 = libev_utils.Timer(cb_check,tb_config.check_time,tb_config.check_time)

timer:start(loop)
timer2:start(loop)
loop:loop()




